const crypto = require('crypto');
const EventEmitter = require('events');
const WebSocket = require('ws');

class Gemini extends EventEmitter {
    constructor(GeminiConfig, client) {
        super();
        this.GeminiConfig = GeminiConfig;
        this.wsClient = client;
        const MAX_RETRY_TIMES = 10;
        this.msgDispenser();
        this.wsClient.on('close', () => {
            this.wsClient.removeAllListeners();
            let timer = 0;

            const autoReconnect = setInterval(() => {
                Gemini.connectWebsocket(this.GeminiConfig)
                    .then(client => {
                        this.wsClient = client;
                        clearInterval(autoReconnect);
                    })
                    .catch(err => {
                        console.log(err);
                        if(timer++ > MAX_RETRY_TIMES){
                            clearInterval(autoReconnect);
                            throw new Error("disconnected");
                        }
                    })
            }, 5000);
        })
    }

    static getWsUrl(GeminiConfig) {
        if(!GeminiConfig || !GeminiConfig['API_KEY'] || !GeminiConfig['GeminiURL'])
            throw new Error('GeminiConfig Err.');
        return `${ GeminiConfig['GeminiURL'] ?? "ws://localhost" }/gemini?auth=${ GeminiConfig['API_KEY'] }`;
    }

    static async createInstance(GeminiConfig) {
        return new Promise((resolve, reject) => {
            if(!GeminiConfig) {
                reject("NULL Param.");
                return;
            }
            this.connectWebsocket(GeminiConfig)
                .then(async client => {
                    resolve(new Gemini(GeminiConfig, client))
                })
                .catch(err => reject(err));
        })
    }

    static async connectWebsocket(GeminiConfig){
        return new Promise((resolve, reject) => {
            const wsUrl = Gemini.getWsUrl(GeminiConfig);
            let client = null;
            try{
                client = new WebSocket(wsUrl);
            }catch(err){
                reject(err);
            }
            if(!client) return;

            client.on("open", () => {
                console.log("gemini open!");
                resolve(client);
            })
        })
    }

    async getReply(id, msg) {
        const stamp = crypto.randomBytes(16).toString('hex');
        return this.wsPost({
            id,
            type: "default",
            prompt: msg,
        }, stamp);
    }

    getReplyStream(id, msg) {
        const stamp = crypto.randomBytes(16).toString('hex');
        this.wsSend({
            id,
            type: "stream",
            prompt: msg,
        }, stamp);
        return stamp;
    }

    async wsPost(params, stamp) {
        return new Promise((resolve, reject) => {
            this.wsClient.on("message", msg => {
                setTimeout(() => {
                    reject("TIMEOUT!");
                }, 5000);

                try { msg = JSON.parse(msg.toString()) }
                catch (err) {}

                if(typeof msg !== 'object') return;
                if(!msg || !msg.stamp || msg.stamp !== stamp) return;
                if(!msg["response"]) return;

                if(!msg["result"]) {
                    reject("NO RESULT");
                    return;
                }
                if(msg["status"] && msg["status"] === "ERROR") {
                    reject("Err: " + JSON.stringify(msg["result"]));
                    return;
                }
                resolve(msg);
            });
            this.wsClient.on("error", (err) => {
                reject(err);
            });

            this.wsSend(params, stamp);
        })
    }

    msgDispenser() {
        const errHandler = (e) => {
            console.log(e);
        }
        this.wsClient.on('message', (msg) => {
            msg = msg.toString();
            if(msg.toLowerCase() === 'ping'){
                this.wsClient.send('Pong');
                console.log("Pong!");
                return;
            }
            try {
                msg = JSON.parse(msg);
            } catch (e) {
                errHandler(e);
            }
            // stream:  {status:OK, response: true, result: answer, piece: enum{start, mid, finish}, stamp: crypto.randomBytes.toHex}
            // default: {status:OK, response: true, result: answer, piece: finish, stamp: crypto.randomBytes.toHex}
            // ping:    "Ping"
            //

            const { status, response, result, piece, stamp } = msg;
            if(!result || !status || !response)
                return errHandler("Wrong Format.");

            switch (status) {
                case 'OK':
                    if(stamp || piece){
                        super.emit(stamp, {result, piece});
                    }
                    break;
                case "ERROR":
                    console.log(msg);
                    return errHandler("Err: " + JSON.stringify(result));
                default:
                    return errHandler("default?");
            }
        })
    }

    wsSend(params, stamp) {
        const ret = {
            stamp,
            request: true,
            ...params,
        }
        this.wsClient.send(JSON.stringify(ret));
    }

}

module.exports = Gemini;
